feat(slack-app phase 1): foundation \u2014 schema + encryption + LLM key endpoints#25
Merged
feat(slack-app phase 1): foundation \u2014 schema + encryption + LLM key endpoints#25
Conversation
…ndpoints
First slice of the Slack app work (see docs/eng-plan-slack-app-v1.md).
Backend-only, no Slack-facing surface yet. Lays the durable foundation
that Phases 2+ build on.
Schema (3 new tables, 3 new migrations):
- 023_llm_keys: customer-supplied LLM provider keys, encrypted at rest.
One row per (team_id|user_id, provider). Partial unique indexes
enforce one-per-scope.
- 024_slack_workspaces: 1-to-1 mapping of Slack workspace → Reflect
team (or solo user). Bot token encrypted at rest. Soft-deleted via
uninstalled_at.
- 025_slack_conversation_state: per-thread short-term agent context,
TTL'd to 24h.
Encryption (src/llm-key-crypto.ts):
- AES-256-GCM with a 12-byte random nonce per write.
- Per-tenant sub-key derived via HKDF-SHA256(masterKey, salt=scopeId,
info='reflect-memory:llm-key-v1') so a leaked encrypted blob is
useless without the team_id/user_id.
- Master key from env RM_LLM_KEY_ENCRYPTION_KEY (64 hex chars / 32
bytes). Lazy validation — service boots without it but LLM-key
features are unavailable until set.
- Auth tag (16-byte GCM tag) appended to ciphertext in the same BLOB.
Service (src/llm-key-service.ts):
- listLlmKeys / getLlmKeySummary / getLlmKeyPlaintext / setLlmKey
(upsert = rotate) / deleteLlmKey.
- Audit events for create / rotate / remove (last4 only — never the
plaintext key).
HTTP routes (admin-only, mirrors /admin/log-export gating):
- GET /admin/llm-keys
- PUT /admin/llm-keys { provider, key }
- DELETE /admin/llm-keys/:provider
Scope auto-resolved from caller's user record (team if present, else
user).
Tests (+20, total 295 → 315 all green):
- HTTP: empty body, unsupported provider, agent-key 403, set echoes
last4, rotate replaces with updated_at advance, double-delete 404,
audit events recorded with last4 (and never plaintext).
- Unit: encrypt/decrypt round-trip, wrong-scope decrypt fails (HKDF
separation), tampered ciphertext fails (GCM), KeyScope validation,
empty plaintext rejected, malformed master key rejected, missing
master key rejected.
Refs: parent memory d959bc61 (Eng Plan: Slack App v1).
Made-with: Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First slice of the Slack app work (full design:
docs/eng-plan-slack-app-v1.md, parent memoryd959bc61). Backend-only, no Slack-facing surface yet — lays the durable foundation that Phases 2+ build on.Schema (3 new tables)
llm_keys(migration 023) — customer LLM provider keys, encrypted at rest. Partial unique indexes enforce one key per (scope, provider).slack_workspaces(migration 024) — 1-to-1 mapping Slack workspace → Reflect team or solo user. Bot token encrypted; soft-delete viauninstalled_at.slack_conversation_state(migration 025) — per-thread agent context, TTL'd to 24h.Encryption (
src/llm-key-crypto.ts)HKDF-SHA256(masterKey, salt=scopeId, info='reflect-memory:llm-key-v1'). Means a leaked encrypted blob is useless without knowing the team_id/user_id.RM_LLM_KEY_ENCRYPTION_KEY(64 hex chars / 32 bytes). Already rolled to dev + prod.envfiles, distinct keys per env. Backups taken.Service (
src/llm-key-service.ts)listLlmKeys/getLlmKeySummary/getLlmKeyPlaintext/setLlmKey(upsert = rotate) /deleteLlmKey. Audit events for create / rotate / remove (last4only — never plaintext).HTTP routes (admin-only)
Mirrors the
/admin/log-exportgating pattern (ownerUserIds.has(request.userId)):GET /admin/llm-keysPUT /admin/llm-keys— body{ provider, key }DELETE /admin/llm-keys/:providerScope auto-resolved from the caller's user record (team if present, else user-level for solo).
Tests
315/315 green (was 295). 20 new tests:
updated_atadvance / double-delete 404 / audit events recorded with last4 and never plaintext.What's NOT in this PR (deliberate, follows in Phase 2+)
/slack/install,/slack/oauth/callback,/slack/eventsare all Phase 2/3.Test plan
tryValidateMasterKeylogs "OK" (no warning) since the env var is set.api-dev.reflectmemory.com:GET /admin/llm-keysreturns{supported_providers: ["anthropic"], keys: []}for an admin token.PUT /admin/llm-keyswith a fake key returnslast4.GETagain shows the key with last4.DELETE /admin/llm-keys/anthropicremoves it.Made with Cursor